并发(concurrency):多条指令在多个处理器上同时执行
并行( parallel):多个进程指令被快速轮换执行进程(Process):处于运行过程中的程序(系统进行资源分配和调度的一个独立单位),每个进程有独立的内存空间
线程(Thread):进程的执行单元(CPU 调度和分派的基本单位),线程之间共享堆空间,每个线程有独立的栈空间(共享父进程中的共享变量及部分环境)进程通信方式:管道(pipe)、有名管道(named pipe)、信号量(semophore)、消息队列(message queue)、信号(sinal)、共享内存(shared memory)、套接字(socket)
操作系统可以同时执行多个任务,每个任务就是进程;进程可以同时执行多个任务,每个任务就是线程
线程调度:JVM 负责线程的调度,采用的是抢占式调度,而不是分时调度
Java 程序运行时至少启动了 2 个线程:主线程 main、垃圾回收线程(后台线程)
多线程是为了同步完成多项任务,不是为了提高程序运行效率,而是通过提高资源使用效率来提高系统的效率
# 同步 异步 阻塞 非阻塞
- 同步/异步:数据如果尚未就绪,是否需要等待数据结果。任务是否在同一个线程中执行。
- 阻塞/非阻塞:进程/线程需要操作的数据如果尚未就绪,是否妨碍了当前进程/线程的后续操作。异步执行任务时,线程是否会阻塞等待结果。
- 同步与异步
- 同步和异步关注的是消息通信机制(synchronous communication/ asynchronous communication)
- 所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回,但是一旦调用返回,就得到返回值了。换句话说,就是由调用者主动等待这个调用的结果。
- 而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果,而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。
- 同步和异步最主要的区别在于任务或接口调用完成时消息通知的方式。
- 线程阻塞与非阻塞
- 阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态
- 阻塞调用是指调用结果返回之前,当前线程会被挂起,调用线程只有在得到结果之后才会返回
- 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程
- 阻塞和非阻塞最主要的区别在于当前线程发起任务或接口调用后是否能继续执行。
名称 | 说明 |
---|---|
同步阻塞 | 当前线程在得不到调用结果前不返回,当前线程进入阻塞态等待 |
同步非阻塞 | 当前线程在得不到调用结果前不返回,但当前线程不阻塞,一直在 CPU 中运行 |
异步阻塞 | 当前线程调用其它线程,当前线程自己并不阻塞,但其它线程会阻塞来等待结果 |
异步非阻塞 | 当前线程调用其它线程,其它线程一直在运行,直到得出结果 |
# 进程的创建和启动
- Runtime 类中的 exec 方法,如
Runtime.getRuntime().exec("notepad");
- ProcessBuilder 类中的 start 方法,如
new ProcessBuilder("notepad").start();
# Thread
- 实现了 Runnable 接口
- 所有的线程对象都必须是 Thread 类或其子类的实例
# 构造器
- Thread()、Thread(Runnable target)、Thread(String name)、Thread(Runnable target, String name)
- Thread(ThreadGroup group, Runnable target, String name):在指定的线程组中创建线程
# 类方法
Thread currentThread()
:返回当前正在执行的线程对象void sleep(long millis)
:让当前正在执行的线程暂停 millis 毫秒,并进入阻塞状态(线程睡眠)(该方法声明抛出了 InterruptedException 异常)void yield()
:暂停当前正在执行的线程对象,转入就绪状态(线程让步)void setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)
:设置当线程由于未捕获到异常而突然终止,并且没有为该线程定义其他处理程序时所调用的默认处理程序
# 实例方法
void start()
:使该线程开始执行,Java 虚拟机调用该线程的 run 方法,只能被处于新建状态的线程调用,否则会引发 IllegalThreadStateException 异常void run()
:如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回void setName(String name)
:为线程设置名字,在默认情况下,主线程的名字为 main,用户启动的多个线程的名字依次为 Thread-0、Thread-1、Thread-2、...、Thread-n 等String getName()
:返回调用该方法的线程名字void join()
:等待调用该方法的线程执行完成,而当前正在执行的线程进入阻塞状态(联合线程)(该方法声明抛出了 InterruptedException 异常)void setDaemon(boolean on)
:on 为"true"时,将该线程设置成守护线程(程序退出时会被回收),该方法必须在 start() 之前调用,否则会引发 IllegalThreadStateException 异常boolean isDaemon()
:判断该线程是否为守护线程int getPriority()
:返回线程的优先级void setPriority(int newPriority)
:更改线程的优先级(范围是 1~10 之间)boolean isAlive()
:测试线程是否处于活动状态void setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)
:设置该线程由于未捕获到异常而突然终止时调用的处理程序
# 线程的创建和启动
# 继承 Thread 类创建线程类
- 使用继承 Thread 类的方法来创建线程类时,多个线程之间无法共享线程类的实例变量
// 定义 Thread 类的子类
public class MyThread extends Thread {
// 重写 Thread 类中的 run() 方法,线程执行体
public void run() {
}
}
public class Demo {
public static void main(String[] args) {
Thread t = new MyThread(); // 创建 Thread 子类的对象
t.start(); // 调用线程对象的 start() 方法来启动该线程
}
}
// 使用匿名内部类的方式创建
new Thread() {
public void run() {
}
}.start();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 实现 Runnable 接口创建多线程
- 采用 Runnable 接口的方式创建的多个线程可以共享同一个 target 对象的实例变量
void run()
:使用实现接口 Runnable 的对象创建一个线程时,启动该线程将导致在独立执行的线程中调用对象的 run 方法
// 定义 Runnable 接口的实现类
public class MyRunnable implements Runnable {
// 重写 Runnable 接口中的 run() 方法,线程执行体
public void run() {
}
}
public class Demo {
public static void main(String[] args) {
Runnable target = new MyRunnable(); // 创建 Runnable 实现类的对象 target
Thread t = new Thread(target, "线程名"); // 将 target 作为运行目标来创建创建 Thread 类的对象
t.start();; // 调用线程对象的 start() 方法来启动该线程
}
}
// 使用匿名内部类的方式创建
new Thread(new Runnable() {
public void run() {
}
}).start();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 使用 Callable 和 FutureTask 创建线程
# Callable<V> 接口
- Callable<V> 接口提供了一个 call() 方法(可以有返回值,可以声明抛出异常)可以作为线程执行体,Callable 接口里的泛型形参类型与 call() 方法返回值类型相同
V call()
:计算结果,如果无法计算结果,则抛出一个异常
# Future<V> 接口
- Future<V> 接口代表 Callable 接口里 call() 方法的返回值,表示异步计算的结果
- Future<V> 接口的常用方法
V get()
:返回 Callable 任务里 call() 方法的返回值,如果计算时抛出异常将会抛出 ExecutionException 异常,如果当前的线程在等待时被中断将会抛出 InterruptedException 异常(调用该方法将导致程序阻塞,必须等到子线程结束后才会得到返回值)
V get(long timeout, TimeUnit unit)
:返回 Callable 任务里 call() 方法的返回值,该方法让程序最多阻塞 timeout 和 unit 指定的时间,如果经过指定时间后 Callable 任务依然没有返回值,将会抛出 TimeoutException 异常
boolean cancel(boolean mayInterruptIfRunning)
:试图取消该 Future 里关联的 Callable 任务
boolean isCancelled()
:如果在 Callable 任务正常完成前被取消,则返回 true
boolean isDone()
:如果 Callable 任务已完成(由于正常终止、异常或取消),则返回 true
# FutureTask<V> 类
- FutureTask<V> 实现类实现了 RunnableFuture<V> 接口(RunnableFuture<V> 接口继承了 Runnable 接口和Future<V> 接口)
- 构造器:FutureTask(Callable<V> callable)、FutureTask(Runnable runnable, V result)(指定成功完成时 get 返回给定的结果为 result)
- FutureTask#run 方法会将捕获到的 Throwable 异常赋值给 FutureTask#outcome 变量
// 使用 Lambda 表达式创建 Callable<V> 接口的实现类,并实现 Call() 方法
// 使用 FutureTask 来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值
FutureTask<Integer> task = new FutureTask<Integer>((Callable<Integer>) () -> {
// call() 方法可以有返回值
return 100;
});
// 将 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程
new Thread(task, "线程名").start();
try {
// 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值,在最多等待 1 秒之后退出
System.out.println("子线程的返回值:" + task.get(1, TimeUnit.SECONDS));
} catch (Exception e) {
e.printStackTrace();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 创建线程的三种方式对比
继承 Thread 类
- 线程类已经继承了 Thread 类,不能再继承其它父类
- 如果需要访问当前线程,直接使用 this 即可获得当前线程
- 多个线程之间无法共享线程类中的实例变量
实现 Runnable、Callable 接口的方式创建多线程
- 线程类只是实现了 Runnable 接口,还可以继承其它类
- 如果需要访问当前线程,则必须使用 Thread. currentThread() 方法
- 所创建的 Runnable 对象只是线程的 target,而多个线程可以共享同一个 target 对象的实例变量,所以适合多个相同线程来处理同一份资源的情况
# 线程安全
一个类可以被多个线程安全调用就是线程安全的
保证多线程环境下共享的、可修改的状态的正确性
线程安全需要保证几个基本特性:
- 原子性,简单说就是相关操作不会中途被其它线程干扰,一般通过同步机制实现
- 可见性,是一个线程修改了某个共享变量,其状态能够立即被其它线程知晓,通常被解释为将线程本地状态反映到主内存上,volatile 就是负责保证可见性的
- 有序性,是保证线程内串行语义,避免指令重排等
JMM 对原子性的保证
- 保证对除 long 和 double 外的基础数据类型的读写操作是原子性的(32 位操作系统单次操作能处理的最长长度为 32 bit)
- 关键字 synchronized 也可以提供原子性保证(通过 Java 的两个字节码指令 monitorenter 和 monitorexit 来保证)
JMM 对可见性的保证
- synchronized
- volatile:强制变量的赋值会同步刷新回主内存,强制变量的读取会从主内存重新加载,保证不同的线程总是能够看到该变量的最新值
JMM 对有序性的保证
- volatile:阻止指令重排序
- happens-before 原则,如:程序顺序原则(一个线程内必须保证语义串行性)、锁规则(对同一个锁的解锁一定发生在再次加锁之前)、传递性规则、线程启动规则、线程结束规则、中断规则、终止规则等
# 线程同步
- 原子操作(atomic operation):不可被中断的一个或一系列操作
- 只需要对那些会改变共享资源的、不可被中断的操作进行同步即可
- 保证在任一时刻只有一个线程可以进入修改共享资源的代码区,其它线程只能在该共享资源对象的锁池中等待获取锁
- 在 Java 中,每一个对象都拥有一个锁标记(monitor),也称为监视器
- 线程开始执行同步代码块或同步方法之前,必须先获得对同步监视器的锁定才能进入同步代码块或者同步方法进行操作
- 当前线程释放同步监视器:当前线程的同步代码块或同步方法执行结束;遇到 break 或 return 语句;出现了未处理的 Error 或 Exception;执行了同步监视器对象的 wait() 方法或 Thread.join() 方法
- 当前线程不会释放同步监视器:当前线程的同步代码块或同步方法中调用 Thread.sleep()、Thread.yield() 方法;其它线程调用了该线程的 suspend() 方法
sleep 是线程类(Thread)的方法,导致此线程暂停执行指定时间,把执行机会给其它线程,但是监控状态依然保持,到时后会自动恢复。调用 sleep 不会释放对象锁。
wait 是 Object 类的方法,对此对象调用 wait 方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出 notify 方法(或 notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态。
# 同步代码块
语法格式
synchronized(同步监视器对象) { // 需要同步的代码 }
1
2
3通常推荐使用可能被并发访问的共享资源作为同步监视器
静态字段属于类,类级别的锁才能保护;而非静态字段属于类实例,实例级别的锁就可以保护
# 同步方法
- 使用
synchronized
关键字来修饰某个方法,就相当于给调用该方法的对象加了锁 - 对于实例方法,同步方法的同步监视器是 this,即调用该方法的对象
- 对于类方法,同步方法的同步监视器是当前方法所在类的字节码对象(如 ArrayUtil.class)
- 不要使用
synchronized
修饰 run() 方法,而是把需要同步的操作定义在一个新的同步方法中,再在 run() 方法中调用该方法
public class Apple implements Runnable {
private int num = 50;
public void run() {
while (num > 0) {
eat();
}
}
// 同步方法
private synchronized void eat() {
if (num > 0) {
System.out.println(Thread.currentThread().getName() + " 吃了编号为 " + num-- + " 的苹果");
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 同步锁(Lock)
- java.util.concurrent.locks 包中,Lock 替代了 synchronized 方法和语句的使用
- Lock 接口的实现允许锁在不同的作用范围内获取和释放,并允许以任何顺序获取和释放多个锁
- 通常建议使用 finally 块来确保在必要时释放锁
- 常用的实现类(java.util.concurrent.locks 包中)
- ReentrantLock
- 可重入锁,即当前持有该锁的线程能够多次获取该锁,无需等待(可以在递归算法中使用)
- 先获取到锁,再进入
try {...}
代码块,最后使用 finally 保证释放锁 - 可以使用 tryLock() 尝试获取锁
- ReentrantReadWriteLock
- 可重入读写锁
- 只允许一个线程写入;允许多个线程在没有写入时同时读取
- 适合读多写少的场景
- StampedLock
- 支持三种模式:写锁、悲观读锁、乐观读
- 把读锁细分为乐观读和悲观读,进一步提升并发效率
- 不是不可重入锁
- ReentrantLock
class Apple implements Runnable {
private int num = 50;
private final Lock lock = new ReentrantLock();
public void run() {
while (num > 0) {
lock.lock();
try {
if (num > 0) {
System.out.println(Thread.currentThread().getName() + " 吃了编号为 " + num-- + " 的苹果");
}
} finally {
lock.unlock();
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Lock 和 synchronized 的选择
- Lock 是一个接口,而 synchronized 是 Java 中的关键字,synchronized 是内置的语言实现
- synchronized 实现共享数据可见性的方式:JVM 底层实现的内置锁(在获取锁后会使本地内存的缓存值失效,在释放锁时会将更改后的所有共享变量副本值写回主内存)
- Lock 接口实现类实现共享数据可见性的方式:通过使用 volatile 修饰 state
- synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock 去释放锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁
- Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时,等待的线程会直等待下去,不能够响应中断
- 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到
- Lock 可以提高多个线程进行读操作的效率
- 在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竟争),此时 Lock 的性能要远远优于 synchronized,因此在具体使用时要根据适当情况选择
# 线程通信
# 线程通信机制
并发模型 | 通信机制 | 同步机制 |
---|---|---|
共享内存 | 线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信 | 同步是显式进行的,即必须显式指定某个方法或某段代码需要在线程之间互斥执行 |
消息传递 | 线程之间通过显式的发送消息来达到交互目的,如 Actor 模型 | 由于消息的发送必须在消息的接收之前,因此同步是隐式进行的 |
- Java 的线程间通过共享内存的方式进行通信
# 使用 Object 类中的方法
- Object 类中用于操作线程通信的实例方法
wait()
:调用该方法的当前线程会释放对该同步监视器(调用者)的锁定,JVM 把该线程存放到等待池中,等待其他的线程唤醒该线程(该方法声明抛出了 InterruptedException 异常)(为了防止虚假唤醒,此方法应始终在循环中使用,即被唤醒后需要再次判断是否满足唤醒条件)notify()
:调用该方法的当前线程唤醒在等待池中的任意一个线程,并把该线程转到锁池中等待获取锁notifyAll()
:调用该方法的当前线程唤醒在等待池中的所有线程,并把该线程转到锁池中等待获取锁
- 这些方法必须在同步块中使用,且只能被同步监视器对象来调用,否则会引发 IllegalMonitorStateException 异常
public class ShareResource {
// 标识数据是否为空(初始状态为空)
private boolean empty = true;
// 需要同步的方法
public synchronized void doWork() {
try {
while (!empty) { // 不空,则等待
this.wait();
}
... // TODO
empty = false; // 修改标识
this.notifyAll(); // 通知其它线程
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 使用 Condition 接口中的方法
- java.util.concurrent.locks 包中,Condition 接口中的
await()
、signal()
、signalAll()
方法替代了 Object 监视器方法的使用(await() 方法也声明抛出了 InterruptedException 异常) - 通过 Lock 对象调用 newCondition() 方法,返回绑定到此 Lock 对象的 Condition 对象
public class ShareResource {
// 创建使用 private final 修饰的锁对象
private final Lock lock = new ReentrantLock();
// 获得指定 Lock 对象对应的 Condition
private final Condition cond = lock.newCondition();
// 标识数据是否为空(初始状态为空)
private boolean empty = true;
// 需要同步的方法
public void doWork() {
lock.lock(); // 进入方法后,立即获取锁
try {
while(!empty) { // 判断是否方法阻塞
cond.await();
}
... // TODO
empty = false; // 修改标识
cond.signalAll(); // 通知其它线程
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); // 使用 finally 块释放锁
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 死锁
当两个线程相互等待对方释放同步监视器时就会发生死锁,死锁无法解决,只能避免
一旦出现死锁,所有线程处于阻塞状态,程序无法继续向下执行
避免死锁
- 加锁顺序:所有的线程都以同样的顺序加锁
和释放锁 - 加锁时限:线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁
避免锁自动释放导致逻辑重复执行:
- 单独开一个线程给锁续期
- 保证业务的幂等性:增加一个标记是否被处理的字段;开一张新表,保存已经处理过的流水号
- 加锁顺序:所有的线程都以同样的顺序加锁
定位死锁:利用 jstack 等工具获取线程栈,然后定位相互之间的依赖关系,进而找到死锁
# 线程数据交换
- java.util.concurrent.Exchanger,用于两个线程之间的数据交换(如在线程间交换缓冲区),如果没有另一个线程将给定的对象传送给当前线程,当前线程就一直阻塞,可设置超时或中断
# 线程的生命周期
- 线程对象的状态存放在 Thread 类的内部枚举类 State 中,枚举常量:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED
- 新建、可运行(就绪、运行)、阻塞、等待、计时等待、终止/死亡
# 控制线程
# 线程睡眠
- 让执行的线程暂停一段时间,进入阻塞状态
Thread.sleep(0)
的作用,是“触发操作系统立刻重新进行一次 CPU 竞争”,竞争的结果也许是当前线程仍然获得 CPU 控制权,也许会换成别的线程获得 CPU 控制权
# 联合线程
- 让当前线程等待另一个线程完成,而当前线程进入阻塞状态
# 后台线程 / 守护线程(Daemon Thread)
- 后台线程 / 守护线程 / 精灵线程(Daemon Thread)
- 在后台运行,为其它线程提供服务的线程,如:垃圾回收线程
- 特征:如果所有的前台线程都死亡,后台线程会自动死亡
- 前台线程创建的子线程默认是前台线程,后台线程创建的子线程默认是后台线程
- 当所有非守护线程全部结束时,JVM 才会退出
# 线程优先级
- 优先级的高低只和线程获得执行机会的次数多少有关
- 每个线程默认的优先级都与创建它的父线程的优先级相同
- int 类型的静态常量:MAX_PRIORITY、MIN_PRIORITY、NORM_PRIORITY,值分别是10(最高优先级)、1(最低优先级)、5(默认优先级)
# 线程让步
- 让执行的线程暂停,进入就绪状态
- 当某个线程调用了 yield() 方法暂停之后,只有优先级与当前线程相同,或者优先级比当前线程更高的处于就绪状态的线程才会获得执行的机会
# 定时器
- 在 java.util 包中提供了 Timer 类、TimerTask 类,可以定时执行特定的任务
# 线程组
- ThreadGroup 类,表示一个线程的集合,可以对一组线程进行集中管理(同时控制这批线程)
- 在默认情况下,子线程和创建它的父线程处于同一个线程组内
# ThreadLocal<T>
- 代表一个线程局部变量
- 当运行于多线程环境的某个对象使用 ThreadLocal 维护变量时,ThreadLocal 为每一个使用该变量的线程分配一个独立的变量副本,从而解决多线程中对同一变量的访问冲突
- 其实现的思路:每个线程对象中有一个 ThreadLocal.ThreadLocalMap 类型的变量,key 是 ThreadLocal 对象,value 是该线程的对应变量副本值,由 ThreadLocal 对象进行维护(在调用 ThreadLocal 对象的 set(value) 方法时,将以
this
为 key 存储在本线程的 ThreadLocalMap 里面) - ThreadLocalMap 采用开放寻址法解决 hash 冲突
- 构造器:
ThreadLocal<T>()
:创建一个线程局部变量,ThreadLocal 对象建议使用 static 修饰(这个变量是一个线程内所有操作共有的,所有此类实例共享此静态变量) - 实例方法
protected T initialValue()
:返回此线程局部变量的当前线程的“初始值”T get()
:返回此线程局部变量中当前线程副本中的值void remove()
:移除此线程局部变量中当前线程的值void set(T value)
:设置此线程局部变量中当前线程副本中的值
- 子类 InheritableThreadLocal:在创建子线程时,子线程会接收所有可继承的线程局部变量的初始值,以获得父线程所具有的值
必须回收自定义的 ThreadLocal 变量,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的 ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄露等问题
尽量使用 try-finally 块进行回收
ThreadLocal 对象使用 static 修饰;ThreadLocal 无法解决共享对象的更新问题
private static final ThreadLocal<DateFormat> sdfThreadLocal = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
ThreadLocal<DateFormat> sdfThreadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
objectThreadLocal.set(userInfo);
try {
// ...
} finally {
objectThreadLocal.remove();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 同步机制与 ThreadLocal
- 如果多个线程之间需要共享资源,以达到线程之间的通信功能,就使用同步机制
- 如果仅仅需要隔离多个线程之间的共享冲突,则可以使用 ThreadLocal